/*
 * Copyright (C) 2008 Peter Kling
 *
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 *
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * 'A Kind of Billiard'. If not, see <http://www.gnu.org/licenses/>.
 */

package cg2.misc;

import java.awt.Frame;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.DebugGL;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;

import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.GLUT;

@SuppressWarnings("serial")
public abstract class JOGLWindowWidthMouseNavigation extends Frame implements GLEventListener,
                                                                              MouseListener,
                                                                              MouseMotionListener,
                                                                              MouseWheelListener {
	// generate glu and glut objects
	protected GLU  glu  = new GLU();
	protected GLUT glut = new GLUT();
	
	// maximal font height for glut stroke characters
	private static final double glut_maxCharHeight = 119.05;
	
	// camera position/orientation
	protected double x_camera = 0., y_camera = 0., z_camera = -10.;
	protected double rotx_camera = 0., roty_camera = 0., rotz_camera = 0.;
	
	// we use a frames-per-second animator
	private FPSAnimator animator;
	
	// mouse coordinates from last mouse event
	private int x_mouse_prev, y_mouse_prev;

	public JOGLWindowWidthMouseNavigation() {
		// request bits for stencil buffer
		GLCapabilities glCapabilities = new GLCapabilities();
		glCapabilities.setStencilBits(8);	
		
		// generate canvas, link it to the OpenGL context and add it to this window
		GLCanvas canvas = new GLCanvas(glCapabilities);
		canvas.addGLEventListener(this);
		add(canvas);
		
		// generate animator
		 animator = new FPSAnimator(canvas, 60);;
		
		// stop rendering and shut down the application when window is closed
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				animator.stop();
				System.exit(0);
			}
		});
	}
	
	/**
	 * @brief Applies camera translation and rotation to the current OpenGL matrix.
	 */
	protected void applyViewTransformation(GL gl) {
		gl.glTranslated(x_camera, y_camera, z_camera);
		gl.glRotated(rotx_camera, 1., 0., 0.);
		gl.glRotated(roty_camera, 0., 1., 0.);
		gl.glRotated(rotz_camera, 0., 0., 1.);
	}
	
	/**
	 * @brief Display method for OpenGL.  
	 */
	public abstract void display(GLAutoDrawable drawable);

	/**
	 * @brief Support for on-the-fly screen mode switching (not yet implemented by jogl).
	 */
	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) { /* void */ }

	/**
	 * @brief Draws the given string at the current OpenGL coordinates with characters having at most the specified 
	 *        height.
	 */
	public void drawString(GL gl, String str, double height) {
		gl.glPushMatrix();
		gl.glScaled(height/glut_maxCharHeight, height/glut_maxCharHeight, 1.);
		for (int i=0; i<str.length(); i++) {
			glut.glutStrokeCharacter(GLUT.STROKE_ROMAN, str.charAt(i));
		}
		gl.glPopMatrix();
	}
	
	/**
	 * @brief Draws the given sting at the current OpenGL coordinates with characters having at most height 1.
	 */
	public void drawString(GL gl, String str) {
		drawString(gl, str, 1.);
	}
	
	/**
	 * @brief Initializes the OpenGL context.
	 */
	public void init(GLAutoDrawable drawable) {
		drawable.setGL(new DebugGL(drawable.getGL()));               // use the debug pipeline during development

		// register some input listeners for user interaction
		drawable.addMouseListener(this);
		drawable.addMouseMotionListener(this);
		drawable.addMouseWheelListener(this);
	}
	
	public void mouseClicked(MouseEvent e) { /* void */ }

	/**
	 * @brief For navigation using the mouse.
	 */
	public void mouseDragged(MouseEvent e) {
		// get current mouse coordinates
		int x_mouse = e.getX();
		int y_mouse = e.getY();
		double width_window  = e.getComponent().getSize().getWidth();
		double height_window = e.getComponent().getSize().getHeight();
		
		// rotation
		if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
			rotx_camera += 360. * (double)(y_mouse-y_mouse_prev) / height_window;
			roty_camera += 360. * (double)(x_mouse-x_mouse_prev) / width_window;
		}
		
		// translation
		if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
			x_camera += 5.*(double)(x_mouse-x_mouse_prev) / width_window;
			y_camera -= 5.*(double)(y_mouse-y_mouse_prev) / height_window;
		}

		// update previous mouse coordinates for next event
		x_mouse_prev = x_mouse;
		y_mouse_prev = y_mouse;
	}

	public void mouseEntered(MouseEvent e) { /* void */ }
	public void mouseExited(MouseEvent e)  { /* void */ }
	public void mouseMoved(MouseEvent e)   { /* void */ }

	/**
	 * @brief For navigation using the mouse.
	 */
	public void mousePressed(MouseEvent e) {
		x_mouse_prev = e.getX();
		y_mouse_prev = e.getY();
	}

	public void mouseReleased(MouseEvent e) { /* void */ }

	/**
	 * @brief For navigation using the mouse.
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {
		double factor = 1.005;
		z_camera *= Math.pow(factor, e.getWheelRotation());
	}

	/**
	 * @brief Called on a reshape event.
	 */
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		GL gl  = drawable.getGL();
		
		// set projection
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(30., 1., 0.1, 100.);
		gl.glMatrixMode(GL.GL_MODELVIEW);
	}

	@Override
	public void setVisible(boolean isVisible) {
		super.setVisible(isVisible);
		if (isVisible) {
			animator.start();
		} else {
			animator.stop();
		}
	}
}
